import logging
import re
from typing import Any, List, Optional, Tuple

import asyncssh

from src.application.proxy.enums import AuthorizationType, ReloadType
from src.application.server.enums import ServerPrivacy, ServerType
from src.infrastructure.db.models import AllowedIP, Proxy, Server, VPNServer
from src.infrastructure.ssh.helpers import (
    generate_iponly_proxy_string,
    generate_proxy_string,
    generate_reload_file_content,
)

logger = logging.getLogger(__name__)


class RemoteClient:
    """Client to interact with a remote host via SSH."""

    def __init__(
        self,
        host: str,
        port: int,
        username: str,
        password: str,
    ):
        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self.client = None

    @property
    async def connection(self):
        """Open SSH connection to remote host."""
        try:
            client = await asyncssh.connect(
                host=self.host,
                port=self.port,
                username=self.username,
                password=self.password,
                known_hosts=None,
            )
            logger.info(f"Подключился к серверу {self.host}:{self.port}")
            return client

        except asyncssh.DisconnectError as e:
            logger.error(f"DisconnectError occurred; Error code: {e.code}; Reason: {e.reason}")
        except Exception as e:
            logger.error(f"Unexpected error while connecting to host {self.host}; Error message: {e}")

    @staticmethod
    async def read_file(conn: asyncssh.SSHClientConnection, filepath: str) -> str | list[str] | bool:
        """
        Read file from remote host.
        :param asyncssh.SSHClientConnection conn: Connection to server
        :param str filepath: Path to file hosted on remote server to fetch.
        """

        async with conn.start_sftp_client() as sftp:
            try:
                remote_file = await sftp.open(filepath)
                return await remote_file.read()

            except (OSError, asyncssh.Error, asyncssh.sftp.SFTPFailure) as e:
                logger.exception("SFTP reading file failed: " + str(e))

            except Exception as e:
                logger.exception(f"Unexpected error reading file: {str(e)}")

        return False

    @staticmethod
    async def write_file(conn: asyncssh.SSHClientConnection, filepath: str, content: str) -> bool:
        """
        Write remote file.
        :param str filepath: Path to file hosted on remote server to fetch.
        :param str content: Content of new file
        """

        async with conn.start_sftp_client() as sftp:
            try:
                remote_file = await sftp.open(filepath, "w")
                await remote_file.write(content)
                return True

            except (OSError, asyncssh.Error, asyncssh.sftp.SFTPFailure) as e:
                logger.exception(f"SFTP writing file failed: {str(e)}")

            except Exception as e:
                logger.exception(f"Unexpected error writing file: {str(e)}")

        return False

    @staticmethod
    async def execute_command(conn: asyncssh.SSHClientConnection, command: str) -> str | bool:
        """
        Execute command on server.
        :param List[str] command: Unix command.
        """

        try:
            result = await conn.run(command, check=True)
            if result.exit_status == 0:
                return result.stdout

        except (OSError, asyncssh.Error) as e:
            logger.exception(f"Error executing command: {str(e)}")

        except Exception as e:
            logger.exception(f"Unexpected error executing command: {str(e)}")

        return False

    @staticmethod
    async def write_cron(
        conn: asyncssh.SSHClientConnection,
        cron_file_path: Optional[str] = "/usr/local/cron.txt",
    ) -> bool:
        """
        Replacing crontab from txt file
        :param asyncssh.SSHClientConnection conn: Connection to server
        :param Optional[str] cron_file_path: Path to txt file containing cron jobs
        """
        try:
            cmd = f"crontab < {cron_file_path}"
            result = await conn.run(cmd)
            if result.exit_status == 0:
                return True

        except (OSError, asyncssh.Error) as e:
            logger.exception(f"Error writing cron: {str(e)}")

        except Exception as e:
            logger.exception(f"Unexpected error writing cron: {str(e)}")

        return False

    @staticmethod
    async def get_modem_ip(config_content_lines: list[str], proxy_port: int) -> str | bool:
        """
        Getting internal modem ip for private mobile proxy from 3proxy config file by proxy port
        :param list[str] config_content_lines: 3proxy.cfg content as list of strings
        :param int proxy_port: Port of proxy related to modem
        """
        try:
            pattern = f"-p{proxy_port} "
            for line in config_content_lines:
                if pattern in line:
                    server_line = line
                    modem_ip = re.findall("-e([\S]+)", server_line)[0].strip()
                    return modem_ip

        except Exception as e:
            logger.exception(e)

        return False

    @staticmethod
    async def get_cron_line(cron_file_content: List[str], modem_ip: str) -> str | bool:
        """
        Returns cron job containing modem ip
        :param cron_file_content: Cron file lines
        :param modem_ip: Modem to find
        """
        for cron_line in cron_file_content:
            if modem_ip.replace("00", "") in cron_line:
                return cron_line
        return False

    @staticmethod
    async def get_proxy_block(
        config_content: str,
        proxy_login: Optional[str] = None,
        proxy_password: Optional[str] = None,
        proxy_port: Optional[int] = None,
        find_by_port: bool = False,
    ) -> str | bool:
        """
        Finding proxy block from auth to flush in 3proxy config
        :param str config_content: 3proxy.cfg content
        :param Optional[str] proxy_login: Login of proxy to find
        :param Optional[str] proxy_password: Password or proxy to find
        :param Optional[int] proxy_port: Proxy port on which the block is searched
        :param bool find_by_port: Find proxy block by its port
        """
        try:
            proxy_blocks = re.findall("#*auth\ \w[\w\W]*?flush", config_content)
            if len(proxy_blocks) == 0:
                raise ValueError("В конфиге нет блоков прокси")

            if find_by_port and proxy_port:
                for proxy_block in proxy_blocks:
                    if f"-p{proxy_port} " in proxy_block:
                        return proxy_block
            else:
                for proxy_block in proxy_blocks:
                    if f"{proxy_login}:" in proxy_block and f"{proxy_password}" in proxy_block:
                        return proxy_block

        except Exception as e:
            logger.exception(e)

        return False


async def change_authorization_type(
    server: Server,
    proxies: list[Proxy],
    authorization_type: AuthorizationType,
    old_ip: Optional[AllowedIP] = None,
    new_ip: Optional[AllowedIP] = None,
) -> bool:
    """
    Modifying 3proxy config on selected server
    Adding new line to config if authorization_type == "by_password"
    Adding allowed ip to config if authorization_type == "by_ip_address"
    :param Server server: Instance of Server to connect
    :param list[Proxy] proxies: List of proxies
    :param AuthorizationType authorization_type: Enum value ("by_ip_address", "by_password")
    :param Optional[AllowedIP] old_ip: Old allowed ip address
    :param Optional[AllowedIP] new_ip: New allowed ip for proxies
    """

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )
    async with await client.connection as conn:
        if not conn:
            return False

        config_content = await client.read_file(
            conn,
            server.config_path,
        )

        config_content_lines = config_content.split("\n")

        for proxy in proxies:
            if server.proxy_type == ServerType.mobile and server.proxy_privacy == ServerPrivacy.shared:
                proxy_block = await client.get_proxy_block(
                    config_content, proxy_port=proxy.port_https, find_by_port=True
                )
            else:
                proxy_block = await client.get_proxy_block(config_content, proxy.login, proxy.password)

                if not proxy_block:
                    proxy_block = await client.get_proxy_block(
                        config_content, proxy_port=proxy.port_https, find_by_port=True
                    )
                    if not proxy_block:
                        logger.error(f"Не могу найти блок прокси по порту {proxy.port_https}")
                        return False

            modem_ip = await client.get_modem_ip(config_content_lines, proxy.port_https)

            if not modem_ip:
                logger.error(f"Не нашел модем по порту {proxy.port_https}")
                return False

            if server.proxy_type == ServerType.mobile and server.proxy_privacy == ServerPrivacy.shared:
                logger.info(f"Меняю авторизацию на шаред прокси {proxy.port_https}")
                # Заменяем авторизацию на мобильных шаред прокси
                if "iponly" in proxy_block:
                    allowed_ips = re.findall("auth iponly([\w\W]*?)proxy", proxy_block)[0].strip()
                    if new_ip.ip_address not in allowed_ips:
                        if old_ip:
                            new_allowed_ips = allowed_ips.replace(old_ip.ip_address, new_ip.ip_address)
                            logger.info(f"Заменил старый ip {old_ip.ip_address} на новый {new_ip.ip_address}")
                        else:
                            # Проверяем, есть ли в блоке прокси разрешенные ip
                            # Может быть ситуация, когда блок будет выглядеть так: auth iponly allow * ... flush
                            if len(re.findall("((?:\d+\.){3}\d+)", allowed_ips)) > 0:
                                new_allowed_ips = f"{allowed_ips},{new_ip.ip_address}"
                            else:
                                new_allowed_ips = allowed_ips.replace("*", f"* {new_ip.ip_address}")
                                logger.info("В блоке нет других ip, добавил новый")
                            logger.info(f"Добавил в блок новый ip {new_ip.ip_address}")

                        new_proxy_block = proxy_block.replace(allowed_ips, new_allowed_ips)
                        config_content = config_content.replace(proxy_block, new_proxy_block)

                elif "strong" in proxy_block:
                    auth_proxy_block = generate_iponly_proxy_string(new_ip.ip_address, proxy.port_https, modem_ip)
                    config_content = config_content.replace(proxy_block, auth_proxy_block)
                    logger.info(f"Заменил auth strong на auth iponly с ip {new_ip.ip_address}")
            else:
                # Заменяем авторизацию на всех остальных прокси
                logger.info(f"Меняю авторизацию на приватном прокси {proxy.port_https}")
                if authorization_type == "by_ip_address":
                    if "iponly" in proxy_block:
                        current_ip = re.findall("((?:\d+\.){3}\d+)", proxy_block)[0]
                        # TODO Тут может быть больше 1 ip при переходе с шаредов
                        new_proxy_block = proxy_block.replace(current_ip, new_ip.ip_address)
                        config_content = config_content.replace(proxy_block, new_proxy_block)
                        logger.info(f"Заменил старый ip {current_ip} на новый {new_ip.ip_address}")
                        # Заменяем старый ip в блоке на новый

                    if "strong" in proxy_block:
                        auth_proxy_block = generate_iponly_proxy_string(new_ip.ip_address, proxy.port_https, modem_ip)
                        config_content = config_content.replace(proxy_block, auth_proxy_block)
                        logger.info(f"Заменил auth strong на auth iponly c ip {new_ip.ip_address}")
                        # Заменяем auth strong на auth iponly

                elif authorization_type == "by_password":
                    if "iponly" in proxy_block:
                        auth_strong_proxy_block = generate_proxy_string(
                            proxy.login,
                            proxy.password,
                            proxy.port_https,
                            modem_ip,
                            server.inner_ip_address,
                        )
                        config_content = config_content.replace(proxy_block, auth_strong_proxy_block)
                        logger.info(f"Заменил auth iponly на auth strong с логином {proxy.login}")
                        # Заменяем auth iponly на auth srong

        config_writed = await client.write_file(conn, server.config_path, config_content)
        logger.info("Внес все изменения в конфиг")

    return config_writed


async def change_rotation_type(
    server: Server,
    proxies: list[Proxy],
    rotation_period: int,
    reload_type: ReloadType,
) -> bool:
    """
    Modifying cron file on selected server
    Commenting cron job if reload_type == "by_link"
    Changing cron job period if reload_type == "by_time" and rotation period passed
    :param Server server: Instance of Server to connect
    :param list[Proxy] proxies: List of proxies
    :param int rotation_period: Selected rotation period
    :param ReloadType reload_type: Enum value ("by_link", "by_time")
    """

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )
    async with await client.connection as conn:
        if not conn:
            return False

        config_content = await client.read_file(
            conn,
            server.config_path,
        )
        config_content_lines = config_content.split("\n")

        cron_file = await client.execute_command(conn, "crontab -l")
        cron_file_lines = cron_file.split("\n")

        for proxy in proxies:
            logger.info(f"Меняю ротацию для прокси {proxy.port_https}")

            modem_ip = await client.get_modem_ip(config_content_lines, proxy.port_https)
            modem_ip = modem_ip.replace("00", "")

            if not modem_ip:
                logger.error(f"Не нашел модем по порту {proxy.port_https}")
                return False

            cron_line = await client.get_cron_line(cron_file_lines, modem_ip)

            if not cron_line:
                logger.error(f"Не нашел строку крона по модему {modem_ip}")
                return False

            if reload_type == "by_link":
                if "#" not in cron_line:
                    # Комментируем строку крона если авторизация по ссылке
                    new_cron_line = "#" + cron_line
                    cron_file = cron_file.replace(cron_line, new_cron_line)
                    logger.info(f"Закомментировал cron job для модема {modem_ip}")

            elif reload_type == "by_time":
                # Заменяем период срабатывания задачи крона
                new_cron_line = re.sub(r"(\d+)", str(rotation_period), cron_line.replace("#", ""), count=1)
                cron_file = cron_file.replace(cron_line, new_cron_line)
                logger.info(f"Заменил период cron job для модема {modem_ip} на {str(rotation_period)} минут")

            if proxy.reload_link:
                old_path = f"/var/www/html/{proxy.reload_link}.php"
                new_path = f"/var/www/html/{proxy.reload_link}.php"

                # Проверяем, существует ли файл реконнекта
                command = f"mv {old_path} {new_path}"
                if not await client.execute_command(conn, command):
                    reload_file_content = generate_reload_file_content(server, modem_ip)
                    await client.write_file(
                        conn,
                        f"/var/www/html/{proxy.reload_link}.php",
                        reload_file_content,
                    )
                    logger.info(f"Создан новый файл реконнекта: {proxy.reload_link}.php")

        await client.write_file(conn, "/usr/local/cron.txt", cron_file)
        cron_writed = await client.write_cron(conn)
        logger.info("Внес все изменения в крон")

    return cron_writed


async def change_expired_proxies(
    server: Server,
    proxies: Tuple[Proxy, Proxy] | List[Proxy],
    allowed_ip: Optional[AllowedIP],
) -> bool:
    """
    Replacing old proxy credentials and reload link with new one on selected server
    Restoring cron job and authorization type to default
    :param Server server: Instance of Server to connect
    :param Tuple[Proxy] proxies: Tuple of old proxy and new Proxy instances
    :param Optional[AllowedIP] allowed_ip: Allowed ip address to delete
    """

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )

    async with await client.connection as conn:
        if not conn:
            return False

        config_content = await client.read_file(conn, server.config_path)
        config_content_lines = config_content.split("\n")

        if server.proxy_type == ServerType.mobile:
            cron_file_content = await client.execute_command(conn, "crontab -l")
            cron_file_lines = cron_file_content.split("\n")

        for new_proxy, old_proxy in proxies:
            modem_ip = await client.get_modem_ip(config_content_lines, old_proxy.get("port_https"))

            if not modem_ip:
                logger.error(f"Не нашел модем по порту {new_proxy.port_https}")
                return False

            if server.proxy_type == ServerType.mobile and server.proxy_privacy == ServerPrivacy.private:
                cron_line = await client.get_cron_line(cron_file_lines, modem_ip)
                if not cron_line:
                    logger.error(f"Не нашел строку крона по модему {modem_ip}")
                    return False

                # Раскомментируем задачу в кроне и возвращаем дефолтный интервал ("5 минут")
                new_cron_line = re.sub(r"(\d+)", "5", cron_line.replace("#", ""), count=1)
                cron_file_content = cron_file_content.replace(cron_line, new_cron_line)
                logger.info(f"Вернул дефолную строку крона для модема {modem_ip}")

            if server.proxy_type == ServerType.mobile and server.proxy_privacy == ServerPrivacy.shared:
                proxy_block = await client.get_proxy_block(
                    config_content,
                    proxy_port=old_proxy.get("port_https"),
                    find_by_port=True,
                )
            else:
                proxy_block = await client.get_proxy_block(
                    config_content, old_proxy.get("login"), old_proxy.get("password")
                )

            if not proxy_block:
                logger.info(
                    f"Не нашел прокси блок по логину {old_proxy.get('login')} и паролю {old_proxy.get('password')}"
                )
                # Если блок прокси не найден по логину и паролю, находим по порту
                proxy_block = await client.get_proxy_block(
                    config_content,
                    proxy_port=old_proxy.get("port_https"),
                    find_by_port=True,
                )

            if not proxy_block:
                logger.exception(
                    f"Не могу найти блок прокси по порту {old_proxy.get('port_https')} или по логину {old_proxy.get('login')} и паролю {old_proxy.get('password')}"
                )

            if server.proxy_type == ServerType.mobile and server.proxy_privacy == ServerPrivacy.shared:
                logger.info(f"Архивирую шаред прокси {new_proxy.port_https}")
                # Меняем данные в мобильных шаред прокси
                # Удаляем разрешенный ip из блока прокси
                allowed_ips = re.findall("auth iponly([\w\W]*?)proxy", proxy_block)
                if allowed_ip and len(allowed_ips) > 0:
                    allowed_ips = allowed_ips[0].strip()
                    if "," not in allowed_ips:
                        # Если в allow остался последний ip адрес
                        new_allowed_ips = allowed_ips.replace(allowed_ip.ip_address, "")
                    else:
                        new_allowed_ips = allowed_ips.replace(f",{allowed_ip.ip_address}", "").replace(
                            f"{allowed_ip.ip_address},", ""
                        )
                    new_proxy_block = proxy_block.replace(allowed_ips, new_allowed_ips)
                    logger.info(f"Удалил из блока разрешенный ip {allowed_ip.ip_address}")
                else:
                    # Если юзер не задал разрешенный ip
                    new_proxy_block = proxy_block
                config_content = config_content.replace(proxy_block, new_proxy_block)
            else:
                logger.info(f"Архивирую приватный прокси {new_proxy.port_https}")
                # Меняем данные во всех остальных видах прокси
                if "iponly" in proxy_block:
                    # Заменяем блок auth iponly на auth strong
                    new_proxy_block = generate_proxy_string(
                        new_proxy.login,
                        new_proxy.password,
                        new_proxy.port_https,
                        modem_ip,
                        server.inner_ip_address,
                    )
                    logger.info(f"Заменил auth iponly на auth strong с логином {new_proxy.login}")
                else:
                    # Заменяем в блоке старый логин и пароль на новые
                    if old_proxy.get("login") in proxy_block and old_proxy.get("password") in proxy_block:
                        new_proxy_block = (
                            proxy_block.replace(
                                f"users {old_proxy.get('login')}",
                                f"users {new_proxy.login}",
                            )
                            .replace(
                                f"allow {old_proxy.get('login')}",
                                f"allow {new_proxy.login}",
                            )
                            .replace(
                                f"deny {old_proxy.get('login')}",
                                f"deny {new_proxy.login}",
                            )
                            .replace(
                                f"CL:{old_proxy.get('password')}",
                                f"CL:{new_proxy.password}",
                            )
                            .replace("#", "")
                        )
                    else:
                        logger.info("Заменяю данные по регуляркам")
                        login = re.findall("users ([^:]+)", proxy_block)[0].strip()
                        password = re.findall("CL:([^ ]+)allow", proxy_block)[0].strip()
                        new_proxy_block = (
                            proxy_block.replace(f"users {login}", f"users {new_proxy.login}")
                            .replace(f"allow {login}", f"allow {new_proxy.login}")
                            .replace(f"deny {login}", f"deny {new_proxy.login}")
                            .replace(f"CL:{password}", f"CL:{new_proxy.password}")
                        )
                    logger.info(
                        f"Заменил в блоке логин {old_proxy.get('login')} > {new_proxy.login} и пароль {old_proxy.get('password')} > {new_proxy.password}"
                    )

                config_content = config_content.replace(proxy_block, new_proxy_block)

                if old_proxy.get("reload_link"):  # Заменяем название файла реконнекта
                    old_path = f"/var/www/html/{old_proxy.get('reload_link')}.php"
                    new_path = f"/var/www/html/{new_proxy.reload_link}.php"

                    command = f"mv {old_path} {new_path}"
                    if not await client.execute_command(conn, command):
                        reload_file_content = generate_reload_file_content(server, modem_ip)
                        await client.write_file(
                            conn,
                            f"/var/www/html/{new_proxy.reload_link}.php",
                            reload_file_content,
                        )
                        logger.info(f"Создан новый файл реконнекта: {new_proxy.reload_link}.php")
                    else:
                        logger.info(
                            f"Файл реконнекта переименован c {old_proxy.get('reload_link')} на {new_proxy.reload_link}"
                        )

        config_writed = await client.write_file(conn, server.config_path, config_content)
        logger.info("Внес все изменения в конфиг")

        if server.proxy_type == ServerType.mobile and server.proxy_privacy == ServerPrivacy.private:
            await client.write_file(conn, "/usr/local/cron.txt", cron_file_content)
            cron_writed = await client.write_cron(conn)
            logger.info("Внес все изменения в крон")
            return all([config_writed, cron_writed])
        else:
            return config_writed


async def generate_vpn_config(server: VPNServer, telegram_id: int) -> str | bool:
    """Generating Wireguard config for user"""

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )

    async with await client.connection as conn:
        if not conn:
            return False
        logger.info(f"Генерирую конфиг для юзера {telegram_id}")
        if await client.execute_command(conn, f"bash wireguard-install.sh {telegram_id}"):
            logger.info("Конфиг сгенерирован")
            vpn_config = await client.read_file(conn, f"{telegram_id}-wg0.conf")
            return vpn_config

        return False


async def delete_vpn_config(server: VPNServer, telegram_id: int) -> bool:
    """Deleting expired Wireguard config"""

    client = RemoteClient(
        server.ip_address,
        port=server.ssh_port,
        username=server.login,
        password=server.password,
    )

    async with await client.connection as conn:
        if not conn:
            return False
        logger.info(f"Удаляю впн конфиг юзера {telegram_id}")
        if await client.execute_command(conn, f"rm {telegram_id}-wg0.conf"):
            return True

        return False


async def get_proxies_from_config(
    ip_address: str,
    ssh_port: int,
    login: str,
    password: str,
    config_path: Optional[str] = "/usr/local/3proxy/3proxy.cfg",
) -> list[dict[str, str | Any]]:
    """
    Importing proxies from 3proxy.cfg
    :param Optional[str] config_path: Path to 3proxy.cfg
    """
    proxy_list = []

    client = RemoteClient(
        host=ip_address,
        port=ssh_port,
        username=login,
        password=password,
    )

    async with await client.connection as conn:
        config_content = await client.read_file(conn, config_path)

    logger.info(f"Получаю прокси из конфига сервера {ip_address}")

    for proxy in re.findall("#*auth\ \w[\w\W]*?flush", config_content):
        try:
            login = re.findall("users ([^:]+)", proxy)[0].strip()
            password = re.findall("CL:([^ ]+)allow", proxy)[0].strip()
        except IndexError:
            login = ""
            password = ""
        port = re.findall("-p(\d+)", proxy)[0]
        if not len(port) <= 3 and login != "" and password != "":
            proxy_list.append(
                {
                    "login": login,
                    "password": password,
                    "port": port,
                }
            )

    return proxy_list
